﻿package visualizer.code {
	
	// Imported Classes
	import flash.display.MovieClip;
	import flash.media.Sound;
	import flash.media.SoundChannel;
	import flash.media.SoundTransform;
	import flash.net.URLRequest;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.events.ProgressEvent;
	import flash.external.ExternalInterface;
	
	// Constructor
	public class TrackPlayer extends MovieClip
	{
		// Constants
		// Tracks are stored in a particular playlist
		public const PLAYLIST1:uint = 0;
		public const PLAYLIST2:uint = 1;
		public const PLAYLIST3:uint = 2;
		public const PLAYLIST4:uint = 3;
		public const FRAMERATE:uint = 25;
		
		// Track Loader
		var TL:TrackLoader;
		
		// Array of Audio Tracks, Track Channels, Track Transforms (volume),
		// and Track Positions (if paused)
		var track:Vector.<Sound>;
		var trackChannel:Vector.<SoundChannel>;
		var trackTransform:Vector.<SoundTransform>;
		var trackPosition:Vector.<uint>;
		
		// Visualizer components
		var prevVisualizer:Arrow;
		var nextVisualizer:Arrow;
		var visualizers:Vector.<Visualizer>;
		var visContainer:MovieClip;
		var currentVisualizer:uint;
		
		// Primitive timer
		var frames:uint;
		
		// Constructor
		/*
			public function TrackPlayer()
			Summary: Constructor which creates a 'Track Loader' object that takes care of loading
					 audio files from the database. It initializes the vectors holding the 
					 tracks, track channels, track transforms, and track positions.
			Pre-Conditions:  none
			Post-Conditions: Vectors initialized, TrackLoader initialized
		*/
		public function TrackPlayer()
		{
			// Setup the visualizers
			visualizers = new Vector.<Visualizer>(2,true);
			visualizers[0] = new BradVisualizer();
			visualizers[1] = new TerekVisualizer();
			currentVisualizer = 0;
			visContainer = new MovieClip();
			visContainer.addChild(visualizers[currentVisualizer]);
			addChild(visContainer);
			
			// Setup the visualizer navigation
			prevVisualizer = new Arrow();
			prevVisualizer.stop();
			prevVisualizer.x = 30;
			prevVisualizer.y = 200;
			addChild(prevVisualizer);
			
			nextVisualizer = new Arrow();
			nextVisualizer.stop();
			nextVisualizer.x = 570;
			nextVisualizer.y = 200;
			nextVisualizer.scaleX = -1;
			addChild(nextVisualizer);
						
			// Setup the audio components
			TL = new TrackLoader();
			track = new Vector.<Sound>(4,true);
			trackChannel = new Vector.<SoundChannel>(4,true);
			trackTransform = new Vector.<SoundTransform>(4,true);
			trackPosition = new Vector.<uint>(4,true);
			
			// Initialize variables
			frames = 0;
			for(var i:uint=0;i<4;i++)
			{
				track[i] = null;
				trackChannel[i] = null;
				trackTransform[i] = new SoundTransform();
				trackTransform[i].volume = 0.25;
				trackPosition[i] = 0;
			}
			
			// Setup the event handlers
			setupEvents();
			
			// Setup the JavaScript to ActionScript Callbacks
			setupAPICallbacks();
		}
		
		// -------------------- Track Player API -------------------- //
		
		/*
			public function loadTrack(trackURL:String, playlist:uint):Boolean
			Summary: Loads the audio file specified at 'trackURL' into the
					 playlist referenced by 'playlist'.
			Pre-Conditions:  'trackURL' must be reference a valid track
							 'playlist' must be either PLAYLIST1, PLAYLIST2, PLAYLIST3, or PLAYLIST4
			Post-Conditions: returns true if the track is loaded in the specifed playlist,
							 returns false if an error occured while loading
		*/
		public function loadTrack(trackURL:String, playlist:uint):Boolean
		{
			// Unload any previous track
			if(!unloadTrack(playlist)) return false;
			
			// Load the track to the appropriate playlist
			track[playlist] = TL.loadTrackByURL(trackURL);
			
			// Load the listeners
			setupLoadingListeners(track[playlist]);
			
			// No errors
			return true;
		}
		
		
		/*
			public function playTrack(playlist:uint):Boolean
			Summary: Plays the track in 'playlist' and assigns it to the appropriate channel. 
					 The percentPlayed argument defines the percentage of the track over which 
					 the playhead advances before starting playback.
			Pre-Conditions:  A valid track be loaded in 'playlist',
							 'playlist' must be either PLAYLIST1, PLAYLIST2, PLAYLIST3, or PLAYLIST4
			Post-Conditions: returns true if the track is played and assigned to the appropriate 
							 channel and transform,
							 returns false if an error occured
		*/
		public function playTrack(playlist:uint,resume:Boolean = false,percentPlayed:uint = 0):Boolean
		{
			// Make sure the track is loaded
			if(!trackIsLoaded(playlist)) return false;
			
			// If the track is playing, then pause it so that the
			// track can play from a new position
			if(trackIsPlaying(playlist)) pauseTrack(playlist);
			
			// Decide whether to or not to resume
			if(resume)
			{
				// Resume the track if possible
				// Make sure that the track is not currently being played
				if(trackIsPlaying(playlist)) return false;
				
				// Play the track and assign it to a channel
				trackChannel[playlist] = track[playlist].play(trackPosition[playlist]);
			}
			else
			{
				// Play the track for the specified percentPlayed
				if(percentPlayed < 0 || percentPlayed > 100) percentPlayed = 0;
				var startPosition:uint = track[playlist].length * (percentPlayed/100) ;
			
				// Play the track and assign it to a channel
				trackChannel[playlist] = track[playlist].play(startPosition);
			}			
			
			// Add event listeners to the track channel
			setupPlaybackListeners(trackChannel[playlist]);
			
			// Adjust the volume of the track
			trackChannel[playlist].soundTransform = trackTransform[playlist];
			
			// No errors
			return true;
		}
		
		
		/* 
			public function pauseTrack(playlist:uint):Boolean
			Summary: Pauses the track in 'playlist' by recording its stop position.
					 Playback is resumed with resumeTrack().
			Pre-Conditions:  A valid track loaded in 'playlist',
							 'playlist' must be either PLAYLIST1, PLAYLIST2, PLAYLIST3, or PLAYLIST4.
							 The track must currently be playing
			Post-Conditions: returns true if the track in 'playlist' pauses playback,
							 returns false if an error occured
		*/
		public function pauseTrack(playlist:uint):Boolean
		{	
			// Make sure that the track is currently being played
			if(!trackIsPlaying(playlist)) return false;
			
			// Save the position of the track
			trackPosition[playlist] = trackChannel[playlist].position;
			
			// Stop the track
			trackChannel[playlist].stop();
			
			// Remove the playback listeners
			removePlaybackListeners(trackChannel[playlist]);
			
			// Set the channel to null since it is no longer being played
			trackChannel[playlist] = null;
			
			// No errors
			return true;
		}
		
		
		/*
			public function adjustVolume(pl1Volume:Number,pl2Volume:Number,pl3Volume:Number,pl4Volume:Number):Boolean
			Summary: Adjusts the volumes of each of the playlists.
			Pre-Conditions:  The volume inputs are scaled proportionally to each other, so adjustVolume(0.25,0.25,0.25,0.25)
							 is equivalent to adjustVolume(25,25,25,25). All inputs must be positive numbers.
			Post-Conditions: returns true if the volumes were adjusted
							 returns false if an error occured
		*/
		public function adjustVolume(pl1Volume:Number,pl2Volume:Number,pl3Volume:Number,pl4Volume:Number):Boolean
		{
			// Check for negative volumes
			if(pl1Volume < 0 || pl2Volume < 0 || pl3Volume < 0 || pl4Volume < 0) return false;
			
			// Total Volume
			var totalVolume:Number = pl1Volume+pl2Volume+pl3Volume+pl4Volume;
			
			// Adjust the volumes proportionally
			trackTransform[PLAYLIST1].volume = pl1Volume/totalVolume;
			trackTransform[PLAYLIST2].volume = pl2Volume/totalVolume;
			trackTransform[PLAYLIST3].volume = pl3Volume/totalVolume;
			trackTransform[PLAYLIST4].volume = pl4Volume/totalVolume;
			
			ExternalInterface.call("console.log",trackTransform[PLAYLIST1].volume,trackTransform[PLAYLIST2].volume,
											trackTransform[PLAYLIST3].volume,trackTransform[PLAYLIST4].volume)
			
			// Update any currently playing track channels
			if(trackIsPlaying(PLAYLIST1)) trackChannel[PLAYLIST1].soundTransform = trackTransform[PLAYLIST1];
			if(trackIsPlaying(PLAYLIST2)) trackChannel[PLAYLIST2].soundTransform = trackTransform[PLAYLIST2];
			if(trackIsPlaying(PLAYLIST3)) trackChannel[PLAYLIST3].soundTransform = trackTransform[PLAYLIST3];
			if(trackIsPlaying(PLAYLIST4)) trackChannel[PLAYLIST4].soundTransform = trackTransform[PLAYLIST4];
			
			// No errors
			return true;
		}
		
		
		// -------------------- Private Utility Functions -------------------- //
		
		/*
			private function unloadTrack(playlist:uint):Boolean
			Summary: Unloads the track in 'playlist'. If there is no track in playlist,
					 it simply clears all of the associated buffers.
			Pre-Conditions:  'playlist' must be either PLAYLIST1, PLAYLIST2, PLAYLIST3, or PLAYLIST4
			Post-Conditions: returns true if the track is unloaded in the specifed playlist,
							 returns false if an error occured while unloading
		*/
		private function unloadTrack(playlist:uint):Boolean
		{
			// Make sure the playlist is valid, and return if it's invalid
			if(!playlistIsValid(playlist)) return false;
			
			// If the track is playing, stop it
			if(trackIsPlaying(playlist)) pauseTrack(playlist);
			
			// Remove any outstanding track listeners (so that it won't leak memory)
			if(track[playlist] != null) removeLoadingListeners(track[playlist]);
			if(trackChannel[playlist] != null) removePlaybackListeners(trackChannel[playlist]);
			
			// Clear all playlist information (track transformations deal with the
			// volume in a playlist, so it's not something you want to clear on a
			// track by track basis)
			track[playlist] = null;
			trackChannel[playlist] = null;
			trackPosition[playlist] = 0;
			
			// No errors
			return true;			
		}
		
		
		/*
			private function playlistIsValid(playlist:uint):Boolean
			Summary: Verifies that 'playlist' is on of the valid pre-defined constants 
					 PLAYLIST1, PLAYLIST2, PLAYLIST3, PLAYLIST4.
			Pre-Conditions:  none
			Post-Conditions: returns true is the playlist is valid,
							 returns false if the playlist is invalid
		*/
		private function playlistIsValid(playlist:uint):Boolean
		{
			if(playlist != PLAYLIST1 && playlist != PLAYLIST2 && 
			   playlist != PLAYLIST3 && playlist != PLAYLIST4) return false;
			else return true;
		}
		
		
		/*
			private function trackIsPlaying(playlist:uint):Boolean
			Summary: Checks to see if the track in 'playlist' is currently
					 playing. It's playing if it has been loaded and the 
					 corresponding trackChannel is keeping track of it's progress.
					 When a track ends, it is no longer playing.
			Pre-Conditions:  A valid track must be loaded in 'playlist',
							 'playlist' must be either PLAYLIST1, PLAYLIST2, PLAYLIST3, or PLAYLIST4.
			Post-Conditions: returns true if 'playlist' is playing,
							 returns false if 'playlist' is not playing
		*/
		private function trackIsPlaying(playlist:uint):Boolean
		{
			// Make sure a track has been loaded
			if(!trackIsLoaded(playlist)) return false;
			
			// Check to see if a channel exists for a track
			if(trackChannel[playlist] == null) return false;
			
			// The channel exists, so the track must be playing
			// When a new song is loaded, the channel is removed
			return true;
		}
		
		
		/*
			private function trackIsLoaded(playlist:uint):Boolean
			Summary: Checks to see if 'playlist' currently contains a track. This means 
					 a track was loaded in that position at some point in time.
			Pre-Conditions:  'playlist' must be either PLAYLIST1, PLAYLIST2, PLAYLIST3, or PLAYLIST4.
			Post-Conditions: returns true if 'playlist' contains a track,
							 returns false if 'playlist' does not contain a track
		*/
		private function trackIsLoaded(playlist:uint):Boolean
		{
			// Make sure the playlist is valid, and return if it's invalid
			if(!playlistIsValid(playlist)) return false;
			
			// Check to see if the track still has an event listener for loading
			if(track[playlist] == null || track[playlist].hasEventListener(Event.COMPLETE)) return false;
			
			// A track is loaded
			return true;
		}
		
		
		/*
			private function setupAPICallbacks():Boolean
			Summary: Registers the JavaScript to ActionScript callback functions
			Pre-Conditions:  none
			Post-Conditions: returns true if the callbacks are successfully set up,
							 returns false if the flash player doesn't support callbacks
		*/
		private function setupAPICallbacks():Boolean
		{
			// Make sure that the version of flash player attempting respond
			// to the callbacks has the necessary capability
			if(!ExternalInterface.available) return false;
			
			// Register the callbacks (we use "this" because there could be other
			// instances of TrackPlayer)
			ExternalInterface.addCallback("loadTrack",this.loadTrack);
			ExternalInterface.addCallback("playTrack",this.playTrack);
			ExternalInterface.addCallback("pauseTrack",this.pauseTrack);
			ExternalInterface.addCallback("adjustVolume",this.adjustVolume);
			
			// No Errors
			return true;
		}
		
				private function timer(e:Event){frames++;}
		
		private function setupLoadingListeners(audioTrack:Sound):void
		{
			// Add listeners to the sound
			audioTrack.addEventListener(ProgressEvent.PROGRESS,onLoadProgress);
			audioTrack.addEventListener(Event.COMPLETE,onLoadComplete);
		}
		
		private function removeLoadingListeners(audioTrack:Sound):void
		{
			// Strip all sound listeners
			audioTrack.removeEventListener(ProgressEvent.PROGRESS,onLoadProgress);
			audioTrack.removeEventListener(Event.COMPLETE,onLoadComplete);
		}
		
		private function onLoadProgress(e:ProgressEvent):void
		{ 			
			// Find the associated playlist
			var audioTrack:Sound = e.target as Sound;
			var playlist:uint = 0;
			for(var i:uint=0;i<track.length;i++)
			{
				if(track[i] == audioTrack)
				{
					playlist = i;
					break;
				}
			}
			
			// Report playlist and percent loaded to java [0,100]
			var percentLoaded:Number = Math.round(100*(e.bytesLoaded / e.bytesTotal));  
			
			// Notify JavaScript
			ExternalInterface.call("loadProgress",playlist,percentLoaded);
		} 
		
		private function onLoadComplete(e:Event):void 
		{ 
			// Find the associated playlist
			var audioTrack:Sound = e.target as Sound;
			var playlist:uint = 0;
			for(var i:uint=0;i<track.length;i++)
			{
				if(track[i] == audioTrack)
				{
					playlist = i;
					break;
				}
			}
		
			// Remove loading listeners
			removeLoadingListeners(e.target as Sound);
			 
			// Notify JavaScript
			//ExternalInterface.call("test");
			ExternalInterface.call("loadComplete",playlist);
		}
		
		private function onPlaybackComplete(e:Event):void
		{
			// Remove the event listeners
			var channel:SoundChannel = e.target as SoundChannel;
			removePlaybackListeners(channel);
			
			// Find the associated playlist
			var playlist:uint = 0;
			for(var i:uint=0;i<trackChannel.length;i++)
			{
				if(trackChannel[i] == channel)
				{
					playlist = i;
					break;
				}
			}
			
			// Remove the channel and reset the playback position
			trackChannel[playlist] = null;
			trackPosition[playlist] = 0;
			
			// Notify Javascript
			ExternalInterface.call("playbackProgress",playlist,100);
			ExternalInterface.call("playbackComplete",playlist);
		}
		
		private function onPlaybackProgress(e:Event):void
		{
			// Report playback once per second
			if(frames%FRAMERATE == 0)
			{
				// Notify JavaScript about every track that it playing
				for(var playlist:uint=0;playlist<track.length;playlist++)
				{
					if(trackIsPlaying(playlist))
					{
						// Calculate the playback percentage
						var percentPlayed:uint = 100 * (trackChannel[playlist].position / track[playlist].length);
						ExternalInterface.call("playbackProgress",playlist,percentPlayed);
					}
				}
			}
		}
		
		private function setupPlaybackListeners(channel:SoundChannel):void
		{
			// Add the listeners
			channel.addEventListener(Event.SOUND_COMPLETE,onPlaybackComplete);
		}
		
		private function removePlaybackListeners(channel:SoundChannel):void
		{
			// Remove the listeners
			channel.removeEventListener(Event.SOUND_COMPLETE,onPlaybackComplete);
		}
		
		private function setupEvents():void
		{
			// Setup Timer
			addEventListener(Event.ENTER_FRAME,timer);
			addEventListener(Event.ENTER_FRAME,onPlaybackProgress);
			
			// Setup Previous Arrow Button
			prevVisualizer.addEventListener(MouseEvent.CLICK,prevVisual);
			prevVisualizer.addEventListener(MouseEvent.MOUSE_OVER,overButton);
			prevVisualizer.addEventListener(MouseEvent.MOUSE_OUT,offButton);
			
			// Setup Next Arrow Button
			nextVisualizer.addEventListener(MouseEvent.CLICK,nextVisual);
			nextVisualizer.addEventListener(MouseEvent.MOUSE_OVER,overButton);
			nextVisualizer.addEventListener(MouseEvent.MOUSE_OUT,offButton);
		}
		
		private function prevVisual(e:MouseEvent):void
		{
			// Change the visualizer if there are previous visualizers to display
			if(currentVisualizer != 0)
			{
				visContainer.removeChildAt(0);
				visContainer.addChildAt(visualizers[--currentVisualizer],0);
			}
		}
		
		private function nextVisual(e:MouseEvent):void
		{
			// Change the visualizer if there are subsequent visualizers to display
			if(currentVisualizer != visualizers.length-1)
			{
				visContainer.removeChildAt(0);
				visContainer.addChildAt(visualizers[++currentVisualizer],0);
			}
		}
		
		// Button Apperance
		private function overButton(e:MouseEvent):void
		{
			var mc:MovieClip = e.target as MovieClip;
			mc.gotoAndStop(2);
		}
		
		private function offButton(e:MouseEvent):void
		{
			var mc:MovieClip = e.target as MovieClip;
			mc.gotoAndStop(1);
		}
	}
}